#!/usr/bin/python3
# -*- coding: utf-8 -*-

import sys
import datetime
import threading
import json
import redis


class DataAgent():
    def __init__(self, host: str, port: int):
        self.db_client = None

        self.read_list = []
        self.read_cache = {}

        self.connect(host, port)
        self.ps = self.db_client.pubsub()
        self.sub_thd_list = []

    def remove_all_sub(self):
        # stop all monitoring threads
        for thd in self.sub_thd_list:
            thd.stop()
            self.sub_thd_list.remove(thd)

    def __del__(self):
        self.remove_all_sub()

    def connect(self, host: str, port: int):
        self.db_client = redis.StrictRedis(host, port, decode_responses=True)

    def disconnect(self):
        pass

    # read a variable from remote DB
    def remote_read_one(self, name):
        return self.db_client.get(name)

    # read variables from remote DB
    def remote_read_many(self, names: list):
        return self.db_client.mget(names)

    # write a variable to remote DB
    def remote_write_one(self, name: str, value: str):
        return self.db_client.set(name, value)

    # write variables to remote DB
    # return True if success
    def remote_write_many(self, name_values):  # input tuple(name, value) list
        if type(name_values) is dict:
            return self.db_client.mset(name_values)
        else:
            raise

    # add a readable point in local cache list
    def local_register(self, name: str):
        if name in self.read_list:
            pass
        else:
            self.read_list.append(name)

    # remove a readable point in local cache list
    def local_unregister(self, name: str):
        # remove it in the read_list
        if name in self.read_list:
            self.read_list.remove(name)
        else:
            print(name, 'is not registered!')
            return False

        # remove it in the read_cache
        if name in self.read_cache:
            del self.read_cache[name]
            return True

    # read all points from remote DB to the local cache
    def pull_to_local(self):
        # force update, return is a list for values
        result = self.remote_read_many(self.read_list)

        # update read_cache one by one
        num = len(self.read_list)
        for i in range(num):  # insert by name index
            self.read_cache[self.read_list[i]] = result[i]

        return self.read_cache

    # read point in the local cache
    def local_read(self, name: str):
        if name in self.read_cache:
            return self.read_cache[name]
        else:
            return None

    def sync(self):
        # read all pull points from remote DB to local read_cache
        if len(self.read_list) > 0:
            return self.pull_to_local()
        else:
            return None

    # observe the change of point, notify the subscriber
    # point value change ==>
    #   callback function map ==>
    #     invoke the computing and redraw functions
    def subscribe(self, name: str, callback_routine):
        self.ps.subscribe(**{name: callback_routine})
        # the subscription launches a thread to monitor the point.
        # the routine is executed when the point value is changed.
        thd = self.ps.run_in_thread(sleep_time=0.1)
        self.sub_thd_list.append(thd)

    # publish value change to all subscriber
    def publish(self, name: str, value):
        self.db_client.publish(name, value)


if __name__ == '__main__':
    if len(sys.argv) > 1:
        host = sys.argv[1]
    else:
        host = 'localhost'

    if len(sys.argv) > 2:
        port = sys.argv[2]
    else:
        port = 6379

    da = DataAgent(host, port)

    # remote write one test
    print('\nSingle point write test:')
    result = da.remote_write_one('rw0', 'v0')
 #  print(type(result), result)
    assert result

    # remote read one test
    print('\nSingle point read test:')
    result = da.remote_read_one('rw0')
 #  print(type(result), result)
    assert result == 'v0'

    # remote write multiple points test
    print('\nMultiple points write test:')
    d = {}
    d['rw1'] = 'v1'
    d['rw2'] = 'v2'
 #  print(type(d), d)
    result = da.remote_write_many(d)
 #  print(type(result), result)
    assert result

    # remote read multiple points test
    print('\nMultiple points read test:')
    result = da.remote_read_many(['rw1', 'rw2'])
 #  print(type(result), result)
    assert result[0] == 'v1'
    assert result[1] == 'v2'

    # register and pull_to_local
    print('\nRegister test:')
    # > register r1 and r2
    da.local_register('r1')
    da.local_register('r2')
    # > pull and query r1 and r2
    result = da.pull_to_local()
 #  print('pull_to_local() result:', type(result), result)
    assert len(result) == 2
    result = da.local_read('r1')
 #  print('r1=', result)
    assert result == 'r11'
    result = da.local_read('r2')
 #  print('r2=', result)
    assert result == 'r12'

    # unregister and pull_to_local
    print('\nUnregister test:')
    # > unregister r2
    da.local_unregister('r2')
    # > pull, then query r1 and r2
    result = da.pull_to_local()
 #  print(type(result), result)
    assert len(result) == 1
    result = da.local_read('r2')
 #  print('r2=', result)
    assert result is None
    result = da.local_read('r1')
 #  print('r1=', result)
    assert result == 'r11'
    # > unregister the r2 again, failure is expected!
    result = da.local_unregister('r2')
    assert result is False
